package com.shiyiwei.test.study.transaction.service;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
 * @Title: TransactionPropagationService
 * @Package: com.shiyiwei.test.study.transaction.service
 * @Description: 事务传播机制理解
 * @author: shiyiwei
 * @date: 2022-01-06 22:18
 * @version: V1.0
 */
@Service("TransactionPropagationService")
public class TransactionPropagationService {

    /*
        传播性	描述
        REQUIRED	如果当前存在事务，则加入该事务；如果当前没有事务，则创建一个新的事务

        SUPPORTS	如果当前存在事务，则加入该事务；如果当前没有事务，则以非事务的方式继续运行

        MANDATORY	如果当前存在事务，则加入该事务；如果当前没有事务，则抛出异常

        REQUIRES_NEW	创建一个新的事务，如果当前存在事务，则把当前事务挂起

        NOT_SUPPORTED	以非事务方式运行，如果当前存在事务，则把当前事务挂起

        NEVER	以非事务方式运行，如果当前存在事务，则抛出异常

        NESTED	如果当前存在事务，则创建一个事务作为当前事务的嵌套事务来运行；
        如果当前没有事务，则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED
    */

    @Resource
    JdbcTemplate jdbcTemplate;

    @Resource
    TransactionPropagationService2 transactionPropagationService2;

    /**
     * 正常情况下执行
     * 同一个事务对lilei这行进行两次写操作，并没有报错锁等待超时
     * 猜测：可能因为mysql行锁是可重入锁，同一个事务绑定锁两次调用相当于可重入锁
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED1() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED1();
    }

    /**
     * 知识点：声明式事务是通过切面来完成的，线程调用目标对象的方法时会被代理对象拦截并且在目标方法前后添加
     * 事务管理的代码，代理对象确认这次事务是否回滚的指标就是目标对象调用时有无抛出异常，如果目标对象抛出异常
     * 到代理对象，代理对象就要调用事务回滚的方法。
     *  this.transactionPropagationService.handleREQUIRED6() 传播行为REQUIRED (方法1)
     *  this.transactionPropagationService2.handleREQUIRED6() 传播行为REQUIRED (方法2)
     *  1.方法1创建事务
     *  2.方法2判断调用他的方法1是有事务的然后方法2会加入方法1创建的事务
     *  3.方法1执行到int i = 1 / 0;这一步会报错，由于方法1加了try...catch 内部消化了异常所以导致事务无法回滚。
     *  4.如果把//throw new RuntimeException("这一步加上，就该回滚了");注释放开事务会回滚，因为重新抛出了
     *  异常，代理对象就可以捕捉到异常然后就可以执行回滚了。
     *  5.如果在方法2中执行int i = 1 / 0;方法1必须要把异常抛给代理对象不然会报错。
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED6() {
        try {
            jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
            this.transactionPropagationService2.handleREQUIRED6();
//            int i = 1 / 0;
        } catch (Exception e) {
            System.out.println("我虽然报错了，但是我就不回滚，就是玩。");
            throw new RuntimeException("这一步加上，就该回滚了");
        }
    }

    /**
     * 知识点
     * 1.mysql行锁，第一个事务绑定表锁并且挂起，第二个事务再获取相同的表锁会等待锁释放，如果第二个事务获取锁
     * 超时会报错导致事务回滚（因为name字段没有加索引所以第一个事务绑定的是表锁否则就是行锁）
     * 2.REQUIRES_NEW事务传播行为，原来存在事务会挂起然后当前方法会创建一个新的事务，等待新事务执行完毕之后原来的
     * 事务才会继续执行
     * TransactionPropagationService.handleREQUIRED2传播级别REQUIRED
     * TransactionPropagationService2.handleREQUIRED2传播级别REQUIRES_NEW
     * 执行过程
     * 1.TransactionPropagationService.handleREQUIRED2创建一个新的事务
     * 2.TransactionPropagationService2.handleREQUIRED2方法创建一个新的事务并且TransactionPropagationService.handleREQUIRED2
     * 事务会挂起
     * 3.TransactionPropagationService2.handleREQUIRED2事务获取锁等待超时然后报错
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED2() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED2();
    }

    /**
     * transactionPropagationService.handleREQUIRED3 传播行为REQUIRED （方法1）
     * transactionPropagationService2.handleREQUIRED3 传播行为REQUIRED （方法2）
     * 执行过程
     * 1.方法1执行的时候创建事务
     * 2.方法2执行会加入方法1创建的事务
     * 3.方法2报错事务回滚方法1也会一块回滚
     * 结论：方法1和方法2在同一个事务中，如果回滚就会一块回滚，满足事务的原子性
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED3() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED3();
    }

    /**
     * transactionPropagationService.handleREQUIRED4 传播行为REQUIRED （方法1）
     * transactionPropagationService2.handleREQUIRED4 传播行为REQUIRED （方法2）
     * 1.方法1执行的时候创建事务
     * 2.方法2执行会加入方法1创建的事务
     * 3.方法2报错事务回滚方法1也会一块回滚
     * 4.方法1加了try...catch处理异常之后就不会再抛出异常然后事务就算报错了也不会回滚
     * 结论：方法1和方法2在同一个事务中，如果回滚就会一块回滚，满足事务的原子性
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED4() {
        try {
            jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
            this.transactionPropagationService2.handleREQUIRED4();
            int i = 1 / 0;
        } catch (Exception e) {
            System.out.println("报错了");
        }
    }

    /**
     * 知识点：以非事务方式运行，如果当前存在事务，则抛出异常
     * transactionPropagationService2.handleREQUIRED5 传播行为NEVER (方法2)
     * transactionPropagationService.handleREQUIRED5 传播行为REQUIRED (方法1)
     * this.handleREQUIRED5tem 传播行为NEVER (方法3)
     * 1.方法1执行的时候会创建一个新的事务
     * 2.方法2执行会报错，因为方法1已经开启事务。
     * 3.程序执行方法3不会报错，因为添加@Transactional标签后，相当于给方法添加了声明式事务，方法所在的类
     * 生成对象时就会生成代理对象，这个代理对象给添加@Transactional的方法加了事务管理的代码，因为方法1
     * 和方法3是同一个代理对象的方法，相当于是同一个对象的方法1内部调用方法3，所以方法3可以看作是方法1内部
     * 的一部分代码，不存在多事务也不会存在传播行为。也就是说方法3@Transactional标签在方法1的内部通过this调用
     * 是无用的。
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED5() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        //报错
        this.transactionPropagationService2.handleREQUIRED5();
        //正确
//        this.handleREQUIRED5tem();
    }

    @Transactional(propagation = Propagation.NEVER,rollbackFor = Exception.class)
    public void handleREQUIRED5tem() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'hanmeimei'");
//        int i = 1 / 0;
//        this.transactionPropagationService2.handleREQUIRED1();
    }

    /**
     * transactionPropagationService.handleREQUIRED7 传播行为REQUIRED (方法1)
     * transactionPropagationService.handleREQUIRED7 传播行为NESTED (方法2)
     *
     * 知识点：
     * 1.NESTED 表示如果当前存在事务，则创建一个事务作为当前事务的嵌套事务来运行；
     * 如果当前没有事务，则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。
     * 2.NESTED 修饰的内部方法属于外部事务的子事务，外部主事务回滚的话，子事务也会回滚，
     * 而内部子事务可以单独回滚而不影响外部主事务和其他子事务（需要处理掉内部子事务的异常）。
     *
     * 执行过程：
     * 1.执行方法1并且方法1创建一个新的事务
     * 2.执行方法2并且方法2创建一个新的内嵌事务
     * 3.方法1执行int i = 1 / 0; 方法1和方法2的事务都会回滚
     * 4.方法2执行int i = 1 / 0; 方法1的事务不会回滚方法2的事务回滚
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED7() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        try {
            this.transactionPropagationService2.handleREQUIRED7();
        } catch (Exception e) {
            e.printStackTrace();
        }
        int i = 1 / 0;
    }

    /**
     * transactionPropagationService.handleREQUIRED8 传播行为REQUIRED (方法1)
     * transactionPropagationService.handleREQUIRED8 传播行为MANDATORY (方法2)
     *
     * 知识点：
     * MANDATORY 表示如果当前存在事务，则加入该事务；如果当前没有事务，则抛出异常。
     *
     * 执行过程：
     * 1.调用方法1并且创建一个新的事务
     * 2.调用方法2，方法2的传播行为是MANDATORY。所以方法2的加入方法1创建的事务。
     * 3.如果把方法1的@Transactional标签去掉的话，在方法1中调用方法2会报错。
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED8() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED8();
    }

    /**
     * transactionPropagationService.handleREQUIRED9 传播行为REQUIRED (方法1)
     * transactionPropagationService.handleREQUIRED9 传播行为SUPPORTS (方法2)
     *
     * 知识点：
     * SUPPORTS 表示如果当前存在事务，则加入该事务；如果当前没有事务，则以非事务的方式继续运行。
     *
     * 执行过程：
     * 1.调用方法1并且创建一个新的事务
     * 2.调用方法2，方法2的传播行为是SUPPORTS。所以方法2的加入方法1创建的事务。
     * 3.如果把方法1的@Transactional标签去掉的话，方法2也不会开启事务。
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED9() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED9();
    }

    /**
     * transactionPropagationService.handleREQUIRED10 传播行为REQUIRED (方法1)
     * transactionPropagationService.handleREQUIRED10 传播行为NOT_SUPPORTED (方法2)
     *
     * 知识点：
     * NOT_SUPPORTED	以非事务方式运行，如果当前存在事务，则把当前事务挂起
     *
     * 执行过程：
     * 1.调用方法1并且创建一个新的事务
     * 2.调用方法2，方法1的事务会挂起
     * 3.方法2执行完成，方法1的事务释放
     * 4.方法2的事务提交
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED10() {
        jdbcTemplate.update("update account set balance=balance+100 where id = 1");
        this.transactionPropagationService2.handleREQUIRED10();
    }

    /**
     * transactionPropagationService.handleREQUIRED11 传播行为REQUIRED (方法1)
     * transactionPropagationService.handleREQUIRED11 传播行为NOT_SUPPORTED (方法2)
     *
     * 知识点：
     * innodb引擎的表支持行锁，前提是更新的查询字段是有索引的，如果更新的查询字段没有索引会使用表锁。
     *
     * 执行过程：
     * 1.调用方法1并且创建一个新的事务
     * 2.调用方法2，方法1的事务会挂起
     * 3.方法2更新数据库操作一直无法执行
     * 4.报错，锁等待超时
     *
     * 原因：
     * 因为方法1和方法2同时对account表进行更新，更新的查询字段name是没有加索引的所以
     * 默认会使用表锁，方法1事务挂起所以一直没有释放表锁，导致方法2一直等待获取表锁
     * 最后发生了死锁，然后锁等待超时报错。
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED11() {
        jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
        this.transactionPropagationService2.handleREQUIRED11();
    }

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED12() {
        jdbcTemplate.update("update account set balance=balance+100 where id = 1");
        try {
            this.transactionPropagationService2.handleREQUIRED12();
        } catch (Exception e) {
            System.out.println("我出错了");
        }

    }
}
